#include "SerialPort.h"
#include <thread>

#define WRITE_TIMEOUT	500
#define READ_TIMEOUT	500
#define RW_MULTIPILER	5

#define RECEIVE_POLLING_INTERVAL 10	

CSerialPort::CSerialPort()
{
	_hCom = INVALID_HANDLE_VALUE;
	_portNum = 0;
}

CSerialPort::~CSerialPort()
{
	Close();
}

/**
 * 
 * @brief	Open serial port and set specified parameters
 * @param	portNo
 * @param	baudRate
 * @param	parity
 * @param	dataBits
 * @param	stopBits
 * @param	flowControl
 */
int CSerialPort::Open(int  portNo, int  baudRate, EParity  parity, int  dataBits, EStopBits  stopsBits, EFlowControl flowControl)
{
	IA_INFO("Open serial port %d", portNo);

	if (IsOpen()) {
		IA_WARN("Port %d already opened, close it", _portNum);
		// We need to reset the parameters do do close the port anyway
		Close();
	}
	SetSerialDataReceiver(NULL);

	_portNum = portNo;

	char portName[32];
	sprintf_s(portName, "COM%d", portNo);

	_hCom = CreateFileA(portName,  
										GENERIC_READ | GENERIC_WRITE, 
										0,											// No share
										NULL,                        
										OPEN_EXISTING,     
										0,
										0);

	IA_ASSERT(_hCom != INVALID_HANDLE_VALUE);

	COMMTIMEOUTS timeOuts;
	if (!GetCommTimeouts(_hCom, &timeOuts)) {
		Close();
		IA_RETURN_ERR
	}
	// @see https://www.ftdichip.com/Support/Knowledgebase/index.html?ft_w32_setcommtimeouts.htm
	timeOuts.ReadIntervalTimeout = 0;
	timeOuts.ReadTotalTimeoutMultiplier = RW_MULTIPILER;
	timeOuts.ReadTotalTimeoutConstant = READ_TIMEOUT;
	timeOuts.WriteTotalTimeoutMultiplier = RW_MULTIPILER;
	timeOuts.WriteTotalTimeoutConstant = WRITE_TIMEOUT;
	if (!SetCommTimeouts(_hCom, &timeOuts)) {
		Close();
		IA_RETURN_ERR
	}

	int status = IA_L_OK;
	status &= SetParam(ESerialPortParams::IA_TAG_SERIAL_PORT_BAUDRATE, baudRate);
	status &= SetParam(ESerialPortParams::IA_TAG_SERIAL_PORT_PPARITY, parity);
	status &= SetParam(ESerialPortParams::IA_TAG_SERIAL_PORT_DATA_BITS, dataBits);
	status &= SetParam(ESerialPortParams::IA_TAG_SERIAL_PORT_STOP_BITS, stopsBits);
	status &= SetParam(ESerialPortParams::IA_TAG_SERIAL_PORT_FLOW_CONTROL, flowControl);

	if (status == IA_L_ERR) {
		Close();
		IA_RETURN_ERR
	}

	Flush();

	if (!_serialSync.try_lock()) {
		IA_INFO("Mutex is locked, we can not continue. closing COM%d", portNo);

		Close();
		IA_RETURN_ERR
	}
	_serialSync.unlock();

	IA_RETURN_OK
}

int CSerialPort::Close() noexcept
{
	IA_INFO("Close serial port %d", _portNum);

	SetSerialDataReceiver(NULL);

	if (_hCom != INVALID_HANDLE_VALUE) {
		CloseHandle(_hCom);
		_hCom = INVALID_HANDLE_VALUE;
	}

	IA_RETURN_OK
}

int CSerialPort::IsOpen() noexcept
{
	if (_hCom == INVALID_HANDLE_VALUE) {
		return IA_L_FALSE;
	}

	// Handle the cases when Close is not called
	// ie: physcilly plug out the line from the socket
	DCB _dcb;
	_dcb.DCBlength = sizeof(DCB);

	_serialSync.lock();
	int isOK = GetCommState(_hCom, &_dcb);
	_serialSync.unlock();

	if (!isOK) {
		IA_RETURN_FALSE
	}

	IA_RETURN_TRUE;
}

int CSerialPort::GetPortNo()
{
	return _portNum;
}

int CSerialPort::SetParam(ESerialPortParams paramName, int val) noexcept
{
	IA_INFO("Set serial port parameter port=%d, param=%d, val=%d", _portNum, paramName, val);

	IA_ASSERT(IsOpen());

	DCB _dcb;
	_dcb.DCBlength = sizeof(DCB);

	_serialSync.lock();
	int isOK = GetCommState(_hCom, &_dcb);
	_serialSync.unlock();
	IA_ASSERT(isOK);

	switch (paramName)
	{
	case ESerialPortParams::IA_TAG_SERIAL_PORT_BAUDRATE:
		_dcb.BaudRate = val;
		break;

	case ESerialPortParams::IA_TAG_SERIAL_PORT_PPARITY:
		switch (val)
		{
		case EParity::NoParity:		_dcb.Parity = NOPARITY; break;
		case EParity::OddParity:		_dcb.Parity = ODDPARITY; break;
		case EParity::EvenParity:	_dcb.Parity = EVENPARITY; break;
		case EParity::MarkParity:	_dcb.Parity = NOPARITY; break;
		case EParity::SpaceParity:	_dcb.Parity = SPACEPARITY; break;

		default: _dcb.Parity = NOPARITY; break;
			break;
		}
		break;

	case ESerialPortParams::IA_TAG_SERIAL_PORT_DATA_BITS:
		_dcb.ByteSize = val;
		break;

	case ESerialPortParams::IA_TAG_SERIAL_PORT_STOP_BITS:
		switch (val)
		{
		case EStopBits::OneStopBit:					_dcb.StopBits = ONESTOPBIT;		break;
		// TODO Set to OnePointFiveStopBits will freeze the application
		case EStopBits::OnePointFiveStopBits:	_dcb.StopBits = ONE5STOPBITS;	break;
		case EStopBits::TwoStopBits:					_dcb.StopBits = TWOSTOPBITS;		break;

		default:
			_dcb.StopBits = ONESTOPBIT;		break;
		}
		break;

	case ESerialPortParams::IA_TAG_SERIAL_PPORT_RECEIVE_BY_LINE:
		IsRecvByLine = (val == 1);
		break;	

	case ESerialPortParams::IA_TAG_SERIAL_PORT_FLOW_CONTROL:
		switch (val)
		{
		case EFlowControl::NoFlowControl:
		{
			_dcb.fRtsControl = RTS_CONTROL_ENABLE;
			_dcb.fDtrControl = DTR_CONTROL_ENABLE;

			_dcb.fOutxCtsFlow = FALSE;
			_dcb.fOutxDsrFlow = FALSE;
			_dcb.fOutX = FALSE;
			_dcb.fInX = FALSE;
			break;
		}
		case EFlowControl::CtsRtsFlowControl:
		{
			_dcb.fOutxCtsFlow = TRUE;
			_dcb.fOutxDsrFlow = FALSE;
			_dcb.fRtsControl = RTS_CONTROL_HANDSHAKE;
			_dcb.fOutX = FALSE;
			_dcb.fInX = FALSE;
			break;
		}
		case EFlowControl::CtsDtrFlowControl:
		{
			_dcb.fOutxCtsFlow = TRUE;
			_dcb.fOutxDsrFlow = FALSE;
			_dcb.fDtrControl = DTR_CONTROL_HANDSHAKE;
			_dcb.fOutX = FALSE;
			_dcb.fInX = FALSE;
			break;
		}
		case EFlowControl::DsrRtsFlowControl:
		{
			_dcb.fOutxCtsFlow = FALSE;
			_dcb.fOutxDsrFlow = TRUE;
			_dcb.fRtsControl = RTS_CONTROL_HANDSHAKE;
			_dcb.fOutX = FALSE;
			_dcb.fInX = FALSE;
			break;
		}
		case EFlowControl::DsrDtrFlowControl:
		{
			_dcb.fOutxCtsFlow = FALSE;
			_dcb.fOutxDsrFlow = TRUE;
			_dcb.fDtrControl = DTR_CONTROL_HANDSHAKE;
			_dcb.fOutX = FALSE;
			_dcb.fInX = FALSE;
			break;
		}
		case EFlowControl::XonXoffFlowControl: // fall through!
		default:
		{
			_dcb.fOutxCtsFlow = FALSE;
			_dcb.fOutxDsrFlow = FALSE;
			_dcb.fOutX = TRUE;
			_dcb.fInX = TRUE;
			_dcb.XonChar = 0x11;
			_dcb.XoffChar = 0x13;
			_dcb.XoffLim = 100;
			_dcb.XonLim = 100;
			break;
		}
		}
		break;

	default:
		IA_INFO("Unknow param name %d with value %d", paramName, val);
		break;
	}

	_serialSync.lock();
	isOK =SetCommState(_hCom, &_dcb);
	_serialSync.unlock();
	IA_ASSERT(isOK);
	
	IA_RETURN_OK
}

int CSerialPort::Flush() noexcept
{
	IA_WARN("Flush serial port %d", _portNum);

	IA_ASSERT(IsOpen());

	_serialSync.lock();
	int isOK = (FlushFileBuffers(_hCom));
	isOK &= (PurgeComm(_hCom, PURGE_TXCLEAR));
	isOK &= (PurgeComm(_hCom, PURGE_RXCLEAR));
	isOK &= (PurgeComm(_hCom, PURGE_TXABORT));
	isOK &= (PurgeComm(_hCom, PURGE_RXABORT));
	_serialSync.unlock();
	IA_ASSERT(isOK);

	IA_RETURN_OK;
}

int CSerialPort::WriteBytes(unsigned char* pData, int len) noexcept
{
	IA_ASSERT(IsOpen());
	
	DWORD bytesWritten = 0;

	_serialSync.lock();
	int isOK = WriteFile(_hCom, pData, len, &bytesWritten, NULL);
	_serialSync.unlock();

	if (!isOK) {
		Flush();

		IA_RETURN_ERR
	}

	// Make sure all bytes are written
	IA_ASSERT(bytesWritten == len);

	IA_RETURN_OK
}

int CSerialPort::WriteString(std::string text)
{
	return WriteBytes((unsigned char*)(&text[0]), (int)text.length());
}

int CSerialPort::BytesWaiting(int* pLen)
{
	*pLen = 0;

	IA_ASSERT(IsOpen());

	COMSTAT stat;
	DWORD dwErrors = 0;

	memset(&stat, 0, sizeof(COMSTAT));

	_serialSync.lock();
	int isOK = ClearCommError(_hCom, &dwErrors, &stat);
	_serialSync.unlock();
	IA_ASSERT(isOK);

	*pLen = stat.cbInQue;

	IA_RETURN_OK
}

/**
 *
 * @brief	Read specified bytes from COM
 * @param	pBuffer			Technically, the size of pData should be bigger than bytesToRead
  * @param	pBufferSize		
 * @param	bytesToRead		How many bytes is designed to read
 */
int CSerialPort::ReadBytes(unsigned char* pBuffer, int bufferSize, int bytesToRead) noexcept
{
	unsigned long actualRead = 0;

	IA_ASSERT(IsOpen());

	IA_ASSERT(bufferSize >= bytesToRead);

	_serialSync.lock();
	int isOK = ReadFile(_hCom, pBuffer, bytesToRead, &actualRead, NULL);
	_serialSync.unlock();
	IA_ASSERT(isOK);
	
	// TODO
	// Do we need to flush the buffer is read failed?

	// Make sure we read the designed bytes of data
	IA_ASSERT(actualRead == bytesToRead);

	IA_RETURN_OK
	
}

/**
 *
 * @brief	This function will start a thread to check receive status
 *				and call the function if any data is received
 * @param	pSerialReceiver		Set to null to disable callback
 *
 */
int CSerialPort::SetSerialDataReceiver(ISerialPortReceiver* pSerialReceiver)
{
	// First let's exit the present thread because we need only one callback
	IsMonitorQuit = IA_L_TRUE;
	if (_receiveThread.joinable())
	{
		_receiveThread.join();
	}

	if (NULL != pSerialReceiver) {
		IsMonitorQuit = IA_L_FALSE;
		_receiveThread = std::thread(ReceiveWorker, this, pSerialReceiver);
	}

	IA_RETURN_OK
}

void CSerialPort::ReceiveWorker(CSerialPort* pSerialPort, ISerialPortReceiver* pSerialReceiver)
{
	ISerialPortReceiver* serialReceiver = pSerialReceiver;
	if (NULL == serialReceiver) {
		IA_INFO("SerialCallback of COM%d is set to null, monitor exitting...", pSerialPort->GetPortNo());
		return;
	}

	unsigned char* pBuffer = NULL;
	int bufferSize = 0;
	int bytesWaitting = 0;

	const int MAX_OFFSET = 1024;
	int offset = 0;

	IA_INFO("Monitor thread on COM%d started..", pSerialPort->GetPortNo());

	int status = IA_L_ERR;
	while (!pSerialPort->IsMonitorQuit && pSerialPort->IsOpen())
	{
		status = pSerialPort->BytesWaiting(&bytesWaitting);
		if (!status) {
			IA_ERROR("BytesWaiting error %d on COM%d, thread exitting...", status, pSerialPort->GetPortNo());

			// @caution exit
			pSerialPort->IsMonitorQuit = IA_L_TRUE;
			break;
		}

		// Wait for a while if there is no data
		if (bytesWaitting < 1) {
			std::this_thread::sleep_for(std::chrono::milliseconds(RECEIVE_POLLING_INTERVAL));
			continue;
		}

		if (bufferSize < bytesWaitting + offset) {
			if (bufferSize == 0) {
				pBuffer = (unsigned char*)malloc(bytesWaitting + offset);
				bufferSize = bytesWaitting + offset;
			}
			else {
				pBuffer = (unsigned char*)realloc(pBuffer, bytesWaitting + offset);
				bufferSize = bytesWaitting + offset;
			}
		}

		// We have to reset the buffer to avoid dirty data from last session
		status = pSerialPort->ReadBytes(pBuffer + offset, bufferSize, bytesWaitting);
		if (!status) {
			IA_ERROR("ReadBytes error %d on COM%d, thread exitting...", status, pSerialPort->GetPortNo());

			// @caution exit
			pSerialPort->IsMonitorQuit = IA_L_TRUE;
			break;
		}

		// Read by line end
		if (pSerialPort->IsRecvByLine && pBuffer[bytesWaitting + offset - 1] != '\n') {	
			if (offset >= MAX_OFFSET) {
				offset = 0;
				continue;
			}
			else{
				offset += bytesWaitting;
				continue;
			}
		}

		IA_INFO("Data received on COM%d : %s", pSerialPort->GetPortNo(), pBuffer);
		status = serialReceiver->HandleData(pBuffer, bytesWaitting + offset);
		if (!status) {
			IA_ERROR("Receive Callback handle error %d on COM%d!", status, pSerialPort->GetPortNo());
			// warn only, no exit!
		}

		offset = 0;
	}

	IA_FREE(pBuffer);
	IA_INFO("Monitor thread on COM%d exitted because quit=%d and isopen=%d", pSerialPort->GetPortNo(), pSerialPort->IsMonitorQuit, pSerialPort->IsOpen());
}
